package io.wins.vertx.web;

import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.RoutingContext;

public class ErrorHandler implements Handler<RoutingContext> {

    private static final Logger logger = LoggerFactory.getLogger(ErrorHandler.class);

    private boolean withStackTrace;

    @Override
    public void handle(final RoutingContext routingContext) {
        final Throwable throwable = routingContext.failure();
        if (throwable != null) {
            ErrorHandler.logger.error("Exception", throwable);
        }
        final int statusCode;
        final ErrorEntity errorEntity;
        if (throwable instanceof WebApplicationException) {
            final WebApplicationException we = (WebApplicationException) throwable;
            statusCode = we.status();
            errorEntity = we.errorEntity() != null ? we.errorEntity() : genericErrorEntity(statusCode);
        } else {
            statusCode = routingContext.statusCode() == -1 ? 500 : routingContext.statusCode();
            errorEntity = genericErrorEntity(statusCode);
        }
        if (withStackTrace && throwable != null) {
            errorEntity.withStackTrace(throwable);
        }
        routingContext.addBodyEndHandler(v -> logRequest(routingContext, errorEntity));
        routingContext.response().setStatusCode(statusCode).putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
                .end(errorEntity.toJsonObject().encode());
    }

    public static ErrorHandler create() {
        return new ErrorHandler();
    }

    public ErrorHandler withStackTrace() {
        withStackTrace = true;
        return this;
    }

    private ErrorEntity genericErrorEntity(final int statusCode) {
        final ErrorEntity.Code errorCode;
        final String errorMessage;
        String url = null;
        if (statusCode == 401) {
            errorCode = ErrorEntity.Code.AUTHENTICATION_ERROR;
            errorMessage = "Authentication failed.";
        } else if (statusCode == 404) {
            errorCode = ErrorEntity.Code.GENERAL_ERROR;
            errorMessage = "The requested resource does not exists.";
        } else if (statusCode >= 400 && statusCode < 500) {
            errorCode = ErrorEntity.Code.GENERAL_ERROR;
            errorMessage = "The request failed processing due to invalid syntax or field values.";
        } else {
            errorCode = ErrorEntity.Code.INTERNAL_ERROR;
            errorMessage = "Unexpected error detected.";
        }
        return ErrorEntity.builder(errorCode).detail(errorMessage).build();
    }

    private void logRequest(final RoutingContext routingContext, final ErrorEntity errorEntity) {
        final JsonObject message = new JsonObject();
        message.put("user", routingContext.user() == null ? new JsonObject() : routingContext.user().principal());
        message.put("param", multiMapToJsonObject(routingContext.request().params()));
        message.put("header", multiMapToJsonObject(routingContext.request().headers()));
        message.put("body", routingContext.getBody() != null && routingContext.getBody().length() > 0
                ? routingContext.getBodyAsJson() : new JsonObject());

        ErrorHandler.logger.error("\nrequest: \"{} {}\"\n{}\nresponse: {}\n{}",
                routingContext.request().rawMethod(), routingContext.request().path(), message.encodePrettily(),
                routingContext.response().getStatusCode(), errorEntity.toJsonObject().encodePrettily());
    }

    private JsonObject multiMapToJsonObject(final MultiMap multiMap) {
        final JsonObject jsonObject = new JsonObject();
        multiMap.forEach(entry -> {
            if (entry.getKey().equals(HttpHeaders.AUTHORIZATION.toString())) {
                jsonObject.put(entry.getKey(), "<AUTHORIZATION>");
            } else {
                jsonObject.put(entry.getKey(), entry.getValue());
            }
        });
        return jsonObject;
    }
}
